#include "core/gamestream/GameStreamManager.h"
#include <cstdint>
#include <deque>
#include <algorithm>
#include <shared_mutex>

#undef LOG_DOMAIN
#undef LOG_TAG
#define LOG_DOMAIN  0x0
#define LOG_TAG     "VideoStatsImpl"
#include <hilog/log.h>


namespace {
constexpr bool DEBUG = false;

using VideoStats = GameStreamManager::VideoStats;
}

VideoStats GameStreamManager::videoStats = VideoStats();

namespace {
struct CondFIFO {
    std::deque<int64_t> data;
    int64_t sum = 0;
    void enqueue(const int64_t &val, const std::function<bool(const CondFIFO &)> &popUntil) {
        data.push_back(val);
        sum += val;
        while (!popUntil(*this)) {
            sum -= data.front();
            data.pop_front();
        }
    }
    double getAvg() const {
        if (data.empty()) {
            return std::numeric_limits<double>::max();
        }
        return 1. * sum / data.size();
    }
};
}

struct VideoStats::Metrics {
    std::shared_mutex mtx;

    static constexpr int AVG_SAMPLE_SIZE = 100;

    int32_t width;
    int32_t height;
    std::string decoder;

    std::unordered_map<int64_t, FrameInfo> onTheFly;

    /** logs the timestamps of frames registered to stats in the past second */
    CondFIFO pastOneSecFramesMs;
    /** logs the data size of frames in the past second */
    CondFIFO pastOneSecDataBytes;
    /** logs the belonging frame of received frames in the past second */
    CondFIFO pastOneSecRemoteFrameNumbers;

    /** host-side processing latency (in 1/10 ms), as reported by Limelight */
    CondFIFO hostProcessingLatenciesTenthMs;
    /** latency to receive the entire decode unit */
    CondFIFO receiveLatencies;
    /** latency queuing in Limelight runtime */
    CondFIFO clientQueueLatencies;
    /** latency of decode */
    CondFIFO decodeLatencies;
};


VideoStats::VideoStats() : m(std::make_unique<Metrics>()) {
}

void VideoStats::clear() {
    m = std::make_unique<Metrics>();
}

void VideoStats::setStreamGeometry(int32_t width, int32_t height) {
    std::unique_lock l(m->mtx);
    m->width = width;
    m->height = height;
}

void VideoStats::setDecoder(const std::string &decoder) {
    std::unique_lock l(m->mtx);
    m->decoder = decoder;
}

void VideoStats::reportRequested(uint64_t id) {
    std::unique_lock l(m->mtx);
    if constexpr (DEBUG) {
        OH_LOG_DEBUG(LOG_APP, "reportRequested idx %{public}d", id);
    }
    m->onTheFly.insert({id, FrameInfo{ .requestedAtMs = static_cast<int64_t>(LiGetMillis()) }});
}

void VideoStats::reportSubmitted(
    uint64_t id,
    int64_t belongToFrame,
    int64_t decodeUnitSize,
    int64_t hostProcessingLatencyTenthMs,
    int64_t enqueuedAtMs,
    int64_t receivedAtMs
) {
    std::unique_lock l(m->mtx);
    if constexpr (DEBUG) {
        OH_LOG_DEBUG(LOG_APP, "reportSubmitted idx %{public}d, decodeUnitSize %{public}d, hostProcessingLatencyTenthMs "
        "%{public}d, enqueuedAtMs %{public}d, receivedAtMs %{public}d",
        id, decodeUnitSize, hostProcessingLatencyTenthMs, enqueuedAtMs, receivedAtMs);
    }
    if (const auto fiit = m->onTheFly.find(id); fiit != m->onTheFly.end()) {
        auto &f = fiit->second;
        if constexpr (DEBUG) {
            OH_LOG_DEBUG(LOG_APP, "reportSubmitted idx %{public}d found", id);
        }
        m->pastOneSecRemoteFrameNumbers.enqueue(belongToFrame, [&] (const auto &q) { return q.data.size() <= m->pastOneSecFramesMs.data.size(); });
        f.decodeUnitSize = decodeUnitSize;
        f.hostProcessingLatencyTenthMs = hostProcessingLatencyTenthMs;
        f.enqueuedAtMs = enqueuedAtMs;
        f.receivedAtms = receivedAtMs;
        f.submittedDecodeAtMs = static_cast<int64_t>(LiGetMillis());
    }
}

void VideoStats::reportDecoded(uint64_t id) {
    std::unique_lock l(m->mtx);
    if constexpr (DEBUG) {
        OH_LOG_DEBUG(LOG_APP, "reportDecoded idx %{public}d", id);
    }
    if (const auto fiit = m->onTheFly.find(id); fiit != m->onTheFly.end()) {
        auto &f = fiit->second;
        if constexpr (DEBUG) {
            OH_LOG_DEBUG(LOG_APP, "reportDecoded idx %{public}d found", id);
        }
        f.decodedAtMs = LiGetMillis();
        m->pastOneSecFramesMs.enqueue(f.enqueuedAtMs, [&] (const auto &q) { return q.data.front() >= f.enqueuedAtMs - 1000; });
        m->pastOneSecDataBytes.enqueue(f.decodeUnitSize, [&] (const auto &q) { return q.data.size() <= m->pastOneSecFramesMs.data.size(); });
        m->hostProcessingLatenciesTenthMs.enqueue(f.hostProcessingLatencyTenthMs, [&] (const auto &q) { return q.data.size() <= Metrics::AVG_SAMPLE_SIZE; });
        m->receiveLatencies.enqueue(f.enqueuedAtMs - f.receivedAtms, [&] (const auto &q) { return q.data.size() <= Metrics::AVG_SAMPLE_SIZE; });
        m->clientQueueLatencies.enqueue(f.submittedDecodeAtMs - f.enqueuedAtMs, [&] (const auto &q) { return q.data.size() <= Metrics::AVG_SAMPLE_SIZE; });
        if (f.decodedAtMs - f.submittedDecodeAtMs < 5 * 1e3) {
            // NOTE since `id` does not necessarily uniquely identifies a frame, we only record metrics that are seemingly reasonable
            m->decodeLatencies.enqueue(f.decodedAtMs - f.submittedDecodeAtMs, [&] (const auto &q) { return q.data.size() <= Metrics::AVG_SAMPLE_SIZE; });
        }
        m->onTheFly.erase(id);
    }
}

napi_value VideoStats::toNapiValue(napi_env env) const {
    std::unique_lock l(m->mtx);
    const char* keys[] = {
        "decoder",
        "width",
        "height",
        "fps",
        "hostProcessingLatencyMs",
        "receiveLatencyMs",
        "clientQueueLatencyMs",
        "decodeLatencyMs",
        "dataRateBytesPerSec",
        "bufferWaitingDecodeCount",
        "bufferDecodingCount",
        "bufferWaitingRenderCount",
        "pastSecRemoteSkipFrameCount",
    };
    if constexpr (DEBUG) {
        OH_LOG_DEBUG(LOG_APP,
            "toNapiValue: past one sec frames %{public}d, host proc lat ms %{public}f, "
            "recv lat ms %{public}f, client queue lat ms %{public}f, decode lat ms %{public}f, "
            "past one sec bytes %{public}d",
            m->pastOneSecFramesMs.data.size(),
            m->hostProcessingLatenciesTenthMs.getAvg() * 0.1,
            m->receiveLatencies.getAvg(),
            m->clientQueueLatencies.getAvg(),
            m->decodeLatencies.getAvg(),
            m->pastOneSecDataBytes.sum
        );
    }
    const napi_value values[] = {
        /*decoder=*/NapiUtils::toNapiValue(env, m->decoder),
        /*width=*/NapiUtils::toNapiValue(env, m->width),
        /*height=*/NapiUtils::toNapiValue(env, m->height),
        /*fps=*/NapiUtils::toNapiValue(env, static_cast<int64_t>(m->pastOneSecFramesMs.data.size())),
        /*hostProcessingLatencyMs=*/NapiUtils::toNapiValue(env, m->hostProcessingLatenciesTenthMs.getAvg() * 0.1),
        /*receiveLatencyMs=*/NapiUtils::toNapiValue(env, m->receiveLatencies.getAvg()),
        /*clientQueueLatencyMs=*/NapiUtils::toNapiValue(env, m->clientQueueLatencies.getAvg()),
        /*decodeLatencyMs=*/NapiUtils::toNapiValue(env, m->decodeLatencies.getAvg()),
        /*dataRateBytesPerSec=*/NapiUtils::toNapiValue(env, m->pastOneSecDataBytes.sum),
        /*bufferWaitingDecodeCount=*/NapiUtils::toNapiValue(env, std::count_if(m->onTheFly.cbegin(), m->onTheFly.cend(), [] (const auto &it) { return it.second.decodeUnitSize <= 0; })),
        /*bufferDecodingCount=*/NapiUtils::toNapiValue(env, std::count_if(m->onTheFly.cbegin(), m->onTheFly.cend(), [] (const auto &it) { return it.second.decodeUnitSize > 0 && it.second.decodedAtMs <= 0; })),
        /*bufferWaitingRenderCount=*/NapiUtils::toNapiValue(env, std::count_if(m->onTheFly.cbegin(), m->onTheFly.cend(), [] (const auto &it) { return it.second.decodedAtMs > 0; })),
        /*pastSecRemoteSkipFrameCount=*/NapiUtils::toNapiValue<int64_t>(env, m->pastOneSecRemoteFrameNumbers.data.empty() ? 0 : m->pastOneSecRemoteFrameNumbers.data.back() - m->pastOneSecRemoteFrameNumbers.data.front() - m->pastOneSecRemoteFrameNumbers.data.size() + 1),
    };
    napi_value obj;
    NAPI_RETURN_IF_NOT_OK(nullptr, napi_create_object_with_named_properties(env, &obj, sizeof(keys) / sizeof(keys[0]), keys, values));
    return obj;
}
